Skip to main content
Version: 1.0.2

gRPC - Authentication and Signature Verification

After FI is ready with the endpoints for inbound message processing, they would have to share the API endpoint URL and credentials. NetXD will use this to configure the connectivity with the FIs core. NetXD will generate the key pair and share the public key with FI.

Introduction to gRPC

The Inbound APIs are built using gRPC (gRPC Remote Procedure Call), a high-performance, open-source RPC framework facilitating communication between clients and servers in a distributed system.

One of the key components of gRPC is its utilization of HTTP/2 as the underlying transport protocol for several advantages, including enhanced performance and efficiency. The HTTP/2 enables multiple requests and responses to be sent over a single connection simultaneously.

  • gRPC relies on Protocol Buffers (Protobuf) as the interface description language. Protocol Buffers provides a structured and efficient way to define the data structures and operations exchanged between clients and servers.

  • gRPC offers a range of advanced features such as Authentication mechanisms that are built into the framework, allowing clients and servers to verify each other's identities before exchanging sensitive information

  • gRPC provides support for load balancing, enabling incoming requests to be distributed across multiple server instances for improved reliability.

Getting started with gRPC

Tools Required

  1. gRPC Runtime - A tool specific to a programming language that allows developers build and uses gRPC services. For example, "grpc-java" is the gRPC runtime for Java, and "grpc-python" is for Python

  2. Protocol Buffers compiler (protoc) - A tool used to compile .proto files

Installing Protocol Buffers Compiler

Install the Protocol Buffers Compiler using a package manager with the following commands.

sudo apt-get install -y protobuf-compiler

Installing Go Packages

Install the Go Packages using a package manager with the following commands.

go get google.golang.org/grpc
go get google.golang.org/protobuf/cmd/protoc-gen-go
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc

Service Definitions

Service definitions are the specifications that outline the structure, methods, and behaviors of the services provided by the server and the corresponding actions that clients can take. These definitions are written in Protocol Buffers (.proto) files.

Click here to download the (.proto) file for this gRPC.

Generating Server Code

The gRPC server runs on the bank side, and the XD Instant Payments will act as a client.

Follow the below steps to generate the Go code for server side:

  1. Download the Protocol Buffers (.proto) file

  2. Navigate to the directory where the (.proto) file is located using cd command in the terminal or command line interface (CLI)

  3. Enter the below command to generate the server code

protoc --go_out=. --go-grpc_out=. pb.proto
note

The server code files are generated in the current directory

Setting up the gRPC server

Create a server file (Example: server.go) & implement the server interface, defined in the generated server code

Example Server Code

package main

import (
"context"
"log"
"net"
"google.golang.org/grpc"

pb "path/of/your/generated/stub"

)

type server struct {
pb.UnimplementedInboundServiceServer
}

func (s *server) CreditTransferInbound(ctx context.Context, in *pb.InboundRequest) (*pb.Response, error) {
log.Printf("CreditTransferInbound Request %v", in)
// business logic to validate and approve or reject the transaction
return &pb.Response{ReferenceNumber: in.ReferenceNumber, TransactionType: in.TransactionType, Status: "ACTC"}, nil
}


func main() {
// Create a listener on TCP port 50051
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

// Create a gRPC server object without TLS/SSL
s := grpc.NewServer() // without TLS/SSL

// Register the service with the server
pb.RegisterInboundServiceServer(s, &server{})

// Start the server
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

Authentication and Security

The gRPC API supports several authentication mechanisms:

  • Secure Sockets Layer/Transport Layer Security (SSL/TLS)
  • Basic Credential Authentication (Basic Auth)
  • Digital Signature

Setting up the gRPC server with SSL/TLS

Using SSL/TLS with gRPC provides a secure channel for communication, by encrypting the data and ensuring trust between clients and servers. Follow the below steps to configure gRPC server with SSL/TLS.

  1. Create a server file (Example: server.go) & implement the server interface, defined in the generated server code

  2. Load the server's certificate and private key, and create the credentials using credentials.NewServerTLSFromFile(certFile, keyFile)

  3. Pass the credentials to the gRPC server using grpc.NewServer(grpc.Creds(creds))

Example Go Server Code

package main

import (
"context"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"

pb "path/of/your/generated/stub"
)

type server struct {
pb.UnimplementedInboundServiceServer
}

func (s *server) CreditTransferInbound(ctx context.Context, in *pb.InboundRequest) (*pb.Response, error) {
log.Printf("CreditTransferInbound Request %v", in)
// business logic to validate and approve or reject the transaction
return &pb.Response{ReferenceNumber: in.ReferenceNumber, TransactionType: in.TransactionType, Status: "ACTC"}, nil
}

func main() {
// Create a listener on TCP port 50051
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

// Load the SSL/TLS Certificates from disk
certFile := "path/to/server.crt"
keyFile := "path/to/server.key"
creds, err := credentials.NewServerTLSFromFile(certFile, keyFile)
if err != nil {
log.Fatalf("failed to load TLS credentials: %v", err)
}

// Create a gRPC server object with the SSL/TLS credentials
s := grpc.NewServer()
s := grpc.NewServer(grpc.Creds(creds))

// Register the service with the server
pb.RegisterInboundServiceServer(s, &server{})

// Start the server
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

Basic Auth and Digital Signature

To use Basic Auth and Digital Signature, include the below metadata in the request payload :

authHeader := "Basic {{base64_encoded_credentials}}"
signature := "{{signature}}"
md := metadata.Pairs(
"authorization", authHeader,
"signature", signature,
)
ctx = metadata.NewOutgoingContext(ctx, md)

Sample code for gRPC Server Signature Verification

package main

import (
"context"
"crypto/ecdsa"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"fmt"
"log"
"math/big"
"net"
"strings"

pb "path/to/your/generated/stub"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/stats"
"google.golang.org/grpc/status"
)

const (
algorithmECDSASHA256 = "ECDSA-SHA256"
signatureHeader = "signature"
)

var publicKey = []byte(`-----BEGIN PUBLIC KEY-----
.....
-----END PUBLIC KEY-----`)

type Verifier struct {
publicKey *ecdsa.PublicKey
}

type payloadKey struct{}

type payload struct {
Data []byte
}

type payloadHandler struct{}

func (h *payloadHandler) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context {
var p payload
return context.WithValue(ctx, payloadKey{}, &p)
}

func (h *payloadHandler) HandleRPC(ctx context.Context, st stats.RPCStats) {
switch s := st.(type) {
case *stats.InPayload:
if p, ok := ctx.Value(payloadKey{}).(*payload); ok {
p.Data = s.Data
}
default:
}
}

func (h *payloadHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context {
return ctx
}

func (h *payloadHandler) HandleConn(_ context.Context, _ stats.ConnStats) {
}

// pass the public key to the newVerifier function
func newVerifier(publicKeyPEM []byte) (*Verifier, error) {
block, _ := pem.Decode(publicKeyPEM)
if block == nil || !strings.Contains(block.Type, "PUBLIC KEY") {
return nil, fmt.Errorf("invalid public key file")
}

publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}

ecdsaPublicKey, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("invalid public key type")
}

return &Verifier{publicKey: ecdsaPublicKey}, nil
}

type server struct {
pb.UnimplementedInboundServiceServer
}

func main() {

lis, err := net.Listen("tcp", ":5455")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}

grpcServer := grpc.NewServer(grpc.StatsHandler(&payloadHandler{}), grpc.UnaryInterceptor(authInterceptor))
pb.RegisterInboundServiceServer(grpcServer, &server{})

log.Printf("gRPC server listening at %v", lis.Addr())

if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}

func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
log.Println("Authenticating request...")
value, ok := ctx.Value(payloadKey{}).(*payload)
if !ok {
return nil, status.Errorf(codes.FailedPrecondition, "failed to extract payload from context")
}
v, _ := newVerifier(publicKey)
if err := v.VerifySignature(ctx, value.Data); err != nil {
log.Printf("Signature verification failed: %v", err)
return &pb.Response{Status: "Signature Verification Failed"}, status.Errorf(codes.FailedPrecondition, "signature verification failed")
}
return handler(ctx, req)
}

func (v *Verifier) VerifySignature(ctx context.Context, rawData []byte) error {

log.Println("Signature verification process started")
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return fmt.Errorf("failed to extract metadata from context")
}
signature := md.Get(signatureHeader)
if len(signature) == 0 {
return fmt.Errorf("no signature found, unable to process request")
}
signatureInfo := parseSignatureInfo(strings.Trim(signature[0], "[]"))

sigBytes, err := base64.StdEncoding.DecodeString(signatureInfo[signatureHeader])
if err != nil {
log.Printf("Error decoding base64 signature: %v", err)
return err
}
switch signatureInfo["algorithm"] {
case algorithmECDSASHA256:
log.Println("Signature algorithm:", algorithmECDSASHA256)
hash := sha256.Sum256(rawData)
var sig struct{ R, S *big.Int }
if _, err := asn1.Unmarshal(sigBytes, &sig); err != nil {
fmt.Println("Error decoding signature:", err)
return fmt.Errorf("error decoding signature")
}
if !ecdsa.Verify(v.publicKey, hash[:], sig.R, sig.S) {
return fmt.Errorf("signature is invalid")
}
log.Println("Signature is valid.")
return nil
default:
return fmt.Errorf("unsupported signature algorithm")
}
}

func parseSignatureInfo(s string) map[string]string {
data := make(map[string]string)
for _, pair := range strings.Split(s, ",") {
parts := strings.SplitN(pair, "=", 2)
if len(parts) != 2 {
log.Printf("Invalid signature info: %s", pair)
continue
}
data[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
return data
}